package com.tduck.cloud.report.data.template;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.tduck.cloud.report.data.enums.AggsTypeEnum;
import com.tduck.cloud.report.data.enums.TimeRangeEnum;
import com.tduck.cloud.report.data.params.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.map.LinkedMap;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.*;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @author : tduck
 * @description : Mongodb数据查询操作
 * @create :  2022/09/22 14:47
 **/
@Slf4j
@Component
@RequiredArgsConstructor
public class MongoDataSearchTemplateImpl implements DataSearchTemplate {
    private final MongoTemplate mongoTemplate;


    public final static String MONGODB_ID = "_id";


    private final String MONGODB_REGEX = "$";

    /**
     * 值是数组的类型
     */
    private final String[] arrayTypes = new String[]{"checkbox"};

    @Override
    public List<Map> groupQuery(GroupQueryParams params) {
        List<AggregationOperation> operations = new ArrayList<>();
        List<String> groupByFields = params.getGroupByFields().stream().map(GroupQueryParams.GroupField::getName).collect(Collectors.toList());
        ProjectionOperation project = handleDocumentProject(params, groupByFields);
        if (CollUtil.isNotEmpty(project.getFields())) {
            operations.add(project);
        }
        // 分組条件
        operations.add(Aggregation.match(buildWhereQuery(params.getFilter())));
        // 排序
        if (CollUtil.isNotEmpty(params.getSortFields())) {
            SortOperation sortOperation = null;
            for (GroupQueryParams.SortField sortField : params.getSortFields()) {
                if (StrUtil.isEmpty(sortField.getName())) {
                    continue;
                }
                if (sortOperation == null) {
                    sortOperation = Aggregation.sort(sortField.getSortType().equals(GroupQueryParams.SortTypeEnum.ASC) ? Sort.Direction.ASC : Sort.Direction.DESC, sortField.getName());
                } else {
                    sortOperation = sortOperation.and(sortField.getSortType().equals(GroupQueryParams.SortTypeEnum.ASC) ? Sort.Direction.ASC : Sort.Direction.DESC, sortField.getName());
                }
            }
            if (sortOperation != null) {
                operations.add(sortOperation);
            }
        }
        String[] groupFields = groupByFields.toArray(new String[0]);
        GroupOperation groupOperation = Aggregation.group(groupFields);
        for (GroupQueryParams.AggregateField field : params.getAggregateFields()) {
            if (ObjectUtil.isNull(field.getAggregateType())) {
                field.setAggregateType(AggsTypeEnum.COUNT);
            }
            switch (field.getAggregateType()) {
                case COUNT:
                    groupOperation = groupOperation.count().as(field.getName() + "_" + field.getAggregateType());
                    break;
                case AVG:
                    groupOperation = groupOperation.avg(field.getName()).as(field.getName() + "_" + field.getAggregateType());
                    break;
                case SUM:
                    groupOperation = groupOperation.sum(field.getName()).as(field.getName() + "_" + field.getAggregateType());
                    break;
                case MIN:
                    groupOperation = groupOperation.min(field.getName()).as(field.getName() + "_" + field.getAggregateType());
                    break;
                case MAX:
                    groupOperation = groupOperation.max(field.getName()).as(field.getName() + "_" + field.getAggregateType());
                    break;
                default:
                    break;
            }
        }

        // 如果是集合  需要unwind转化 不然分组会出现问题
        for (GroupQueryParams.GroupField groupByField : params.getGroupByFields()) {
            if (StrUtil.startWithAny(groupByField.getName(), arrayTypes)) {
                operations.add(Aggregation.unwind(groupByField.getName()));
            }
        }
        operations.add(groupOperation);
        // 返回条数
        if (ObjectUtil.isNotNull(params.getLimit())) {
            operations.add(Aggregation.limit(params.getLimit()));
        }
        Aggregation aggregation = Aggregation.newAggregation(operations);
        AggregationResults<Map> aggregate = mongoTemplate.aggregate(aggregation, params.getQueryObjName(), Map.class);
        return aggregate.getMappedResults();
    }


    /**
     * 处理文档Project 指定需要使用的字段
     * 用来新增字段之类的 只返回project的字段
     */
    private ProjectionOperation handleDocumentProject(GroupQueryParams params, List<String> groupByFields) {
        // 分组字段处理 支持时间按月 按天分组的
        ProjectionOperation project = Aggregation.project();
        for (int i = 0; i < groupByFields.size(); i++) {
            GroupQueryParams.GroupField groupField = params.getGroupByFields().get(i);
            // 根据日期分组
            if (isDateTime(groupField.getName())) {
                String alias = groupField.getName() + "format";
                project = project.and(DateOperators.DateToString.dateOf(ConvertOperators.ToDate.toDate(MONGODB_REGEX + groupField.getName())).toString(groupField.getDateFormat())).as(alias);
                groupByFields.set(i, alias);
            }
        }
        // 筛选条件的字段
        // 处理时间字段条件过滤 存入的为字符串时间 需要转换成Mongo时间
        // 时间字段需要拿出来单独处理 把里面得getFormItemId存储为1一个set
        Set<String> todoIdSet = params.getFilter().stream()
                .filter(filter -> null != filter && CollUtil.isNotEmpty(filter.getConditionList()))
                .flatMap(form -> form.getConditionList().stream()).map(FormDataFilterParams.Condition::getFormItemId).collect(Collectors.toSet());
        // 排序中的时间字段
        if (CollUtil.isNotEmpty(params.getSortFields())) {
            todoIdSet.addAll(params.getSortFields().stream().map(GroupQueryParams.SortField::getName).collect(Collectors.toSet()));
        }
        for (String dateFieldId : todoIdSet) {
            if (isDateTime(dateFieldId)) {
                project = project.and(ConvertOperators.ToDate.toDate(MONGODB_REGEX + dateFieldId)).as(dateFieldId);
            }
        }

        // 其他分组的字段也要加进来 转数组
        String[] groupFields = params.getGroupByFields().stream().map(GroupQueryParams.GroupField::getName).toArray(String[]::new);
        // 条件的字段
        String[] conditionFields = params.getFilter().stream().map(FormDataFilterParams::getConditionList).flatMap(Collection::stream)
                .map(FormDataFilterParams.Condition::getFormItemId)
                .filter(formItemId -> !isDateTime(formItemId)).distinct().toArray(String[]::new);
        // 排序字段 去掉 null
        String[] sortFields = params.getSortFields().stream().map(GroupQueryParams.SortField::getName).filter(StrUtil::isNotEmpty)
                .filter(formItemId -> !isDateTime(formItemId)).toArray(String[]::new);
        // 聚合字段
        String[] aggregateFields = params.getAggregateFields().stream().map(GroupQueryParams.AggregateField::getName).toArray(String[]::new);
        project = project.andInclude(groupFields).andInclude(conditionFields).andInclude(aggregateFields).andInclude(sortFields);
        return project;
    }


    /**
     * 是否是时间字段
     *
     * @param field 字段名
     * @return 是否是时间字段
     */
    private boolean isDateTime(String field) {
        return StrUtil.contains(field, "date") || StrUtil.contains(field, "Time");
    }


    @Override
    public long getFieldTotalValue(ValueQueryParams params) {
        Query query = new Query();
        query.addCriteria(buildWhereQuery(params.getFilter()));
        return mongoTemplate.count(query, Map.class, params.getQueryObjName());
    }

    @Override
    public long getFieldValueCount(ValueQueryParams params) {
        Query query = new Query(Criteria.where(params.getFieldName()).is(params.getValue()).andOperator(buildWhereQuery(params.getFilter())));
        return mongoTemplate.count(query, Map.class, params.getQueryObjName());
    }

    @Override
    public Long getFieldAggregateValue(ValueQueryParams params) {
        String tableName = params.getQueryObjName();
        String fieldName = params.getFieldName();
        AggsTypeEnum typeEnum = params.getAggsType();
        Query query = new Query();
        query.addCriteria(buildWhereQuery(params.getFilter()));
        try {
            switch (typeEnum) {
                case MAX:
                    query.with(Sort.by(Sort.Order.desc(fieldName)));
                    query.fields().include(fieldName);
                    return MapUtil.getLong(mongoTemplate.findOne(query, Map.class, tableName), fieldName, 0L);
                case MIN:
                    query.with(Sort.by(Sort.Order.asc(fieldName)));
                    query.fields().include(fieldName);
                    return MapUtil.getLong(mongoTemplate.findOne(query, Map.class, tableName), fieldName, 0L);
                case COUNT:
                    query.with(Sort.by(Sort.Order.asc(fieldName)));
                    query.fields().include(fieldName);
                    return Convert.convert(Long.class, mongoTemplate.count(query, Object.class, tableName), 0L);
                case SUM:
                    AggregationResults<Map> aggregate = mongoTemplate.aggregate(handleBasicAggregation(fieldName, tableName, params.getFilter(), AggsTypeEnum.SUM),
                            params.getQueryObjName(), Map.class);
                    Object value = Objects.requireNonNull(aggregate.getUniqueMappedResult()).get(fieldName);
                    return Convert.convert(Long.class, value, 0L);
                case AVG:
                    AggregationResults<Map> avgAgg = mongoTemplate.aggregate(handleBasicAggregation(fieldName, tableName, params.getFilter(), AggsTypeEnum.AVG),
                            params.getQueryObjName(), Map.class);
                    value = Objects.requireNonNull(avgAgg.getUniqueMappedResult()).get(fieldName);
                    return Convert.convert(Long.class, value, 0L);
                default:
                    return 0L;
            }
        } catch (Exception e) {
            log.error("getFieldAggregateValue error,tableName:{},fieldName:{},typeEnum:{}", tableName, fieldName, typeEnum, e);
            return 0L;
        }
    }


    /**
     * 处理基础聚合
     *
     * @return
     */
    public Aggregation handleBasicAggregation(String fieldName, String tableName, List<FormDataFilterParams> filterParams, AggsTypeEnum aggsTypeEnum) {
        GroupQueryParams groupQueryParams = new GroupQueryParams();
        groupQueryParams.setQueryObjName(tableName);
        groupQueryParams.setFilter(filterParams);
        groupQueryParams.setAggregateFields(Collections.singletonList(new GroupQueryParams.AggregateField(fieldName, aggsTypeEnum)));
        groupQueryParams.setGroupByFields(Collections.singletonList(new GroupQueryParams.GroupField(fieldName, "")));
        ProjectionOperation project = handleDocumentProject(groupQueryParams, Collections.singletonList(fieldName));
        Aggregation aggregation = Aggregation.newAggregation(project, Aggregation.match(buildWhereQuery(filterParams)), Aggregation.group().sum(fieldName).as(fieldName));
        // 分组字段
        switch (aggsTypeEnum) {
            case AVG:
                aggregation = Aggregation.newAggregation(project, Aggregation.match(buildWhereQuery(filterParams)), Aggregation.group().avg(fieldName).as(fieldName));
                break;
            case SUM:
                aggregation = Aggregation.newAggregation(project, Aggregation.match(buildWhereQuery(filterParams)), Aggregation.group().sum(fieldName).as(fieldName));
                break;
            default:
                break;
        }
        return aggregation;
    }

    @Override
    public List<Object> getFieldDistinctList(String tableName, String fieldName) {
        Query query = new Query();
        return mongoTemplate.findDistinct(query, fieldName, tableName, Object.class);
    }

    @Override
    public List<LinkedMap> queryFieldList(QueryFieldListParams queryFieldListParams) {
        Query query = new Query();
        query.addCriteria(buildWhereQuery(queryFieldListParams.getFilter()));
        query.with(Sort.by(Sort.Order.desc("id")));
        for (String field : queryFieldListParams.getFields()) {
            query.fields().include(field);
        }
        if (ObjectUtil.isNotNull(queryFieldListParams.getLimit())) {
            query.limit(queryFieldListParams.getLimit());
        }
        List<LinkedMap> maps = mongoTemplate.find(query, LinkedMap.class, queryFieldListParams.getQueryObjName());
        // 去除_id字段
        maps.forEach(item -> item.remove(MONGODB_ID));
        return maps;
    }

    @Override
    public List<Map> getProvinceGroupCount(GroupQueryParams params) {
        GroupQueryParams.AggregateField field = CollUtil.getFirst(params.getAggregateFields());
        String name = field.getName();
        String submitAddress = "submitAddress";
        List<AggregationOperation> lstOperations = new ArrayList<>(10);
        lstOperations.add(Aggregation.match(buildWhereQuery(params.getFilter())));
        // 返回条数
        if (ObjectUtil.isNotNull(params.getLimit())) {
            lstOperations.add(Aggregation.limit(params.getLimit()));
        }
        if (name.equalsIgnoreCase(submitAddress)) {
            // 切分地区 以 - 切分
            AggregationOperation splitAgg =
                    Aggregation.project().andExpression("{ $split: {'$submitAddress', '-'}}").as("regions");
            lstOperations.add(splitAgg);
        } else {
            AggregationOperation splitAgg =
                    Aggregation.project().and("$province_city1684829279088.cascadeValue").as("regions");
            lstOperations.add(splitAgg);
        }
        // 取省份
        ProjectionOperation province =
                Aggregation.project("$regions").andExpression("{ $arrayElemAt: { '$regions', 0 }}").as("province");
        lstOperations.add(province);
        // 求总
        AggregationOperation groupAgg = Aggregation.group("$province").count().as("count");
        lstOperations.add(groupAgg);
        // 定义查询内容
        ProjectionOperation projectionOperation =
                Aggregation.project("count").andExclude("_id").and("$_id").as("province");
        lstOperations.add(projectionOperation);
        AggregationOptions aggregationOptions = AggregationOptions.builder().allowDiskUse(true).build();
        //开始查询
        Aggregation agg = Aggregation.newAggregation(lstOperations).withOptions(aggregationOptions);
        AggregationResults<Map> groupResult = this.mongoTemplate.aggregate(agg, params.getQueryObjName(), Map.class);
        return groupResult.getMappedResults();
    }


    /**
     * 构建查询条件
     *
     * @param filters 查询条件
     * @return monngo查询条件
     */
    private Criteria buildWhereQuery(List<FormDataFilterParams> filters) {
        Criteria criteria = Criteria.where(MONGODB_ID).exists(true);
        if (CollUtil.isEmpty(filters)) {
            return criteria;
        }
        for (FormDataFilterParams filter : filters) {
            FormDataFilterParams.RelEnum rel = filter.getRel();
            Criteria subCriteria = buildWhereQuery(filter);
            if (ObjectUtil.isNull(subCriteria)) {
                continue;
            }
            if (rel == FormDataFilterParams.RelEnum.AND) {
                criteria = criteria.andOperator(subCriteria);
            } else {
                criteria = criteria.orOperator(subCriteria);
            }
        }
        return criteria;
    }

    /**
     * 构建查询条件
     *
     * @param filter 查询条件
     * @return monngo查询条件
     */
    private Criteria buildWhereQuery(FormDataFilterParams filter) {
        // 创建查询条件对象
        if (filter == null || CollUtil.isEmpty(filter.getConditionList())) {
            return null;
        }
        List<FormDataFilterParams.Condition> conditionList = filter.getConditionList();
        FormDataFilterParams.RelEnum rel = filter.getRel();
        Criteria criteria = new Criteria();
        List<Criteria> criteriaList = new ArrayList<>();
        if (CollUtil.isNotEmpty(conditionList)) {
            for (FormDataFilterParams.Condition item : conditionList) {
                String formItemId = item.getFormItemId();
                switch (item.getMethod()) {
                    case EQ:
                        criteriaList.add(Criteria.where(formItemId).is(item.getValue()));
                        break;
                    case NE:
                        criteriaList.add(Criteria.where(formItemId).ne(item.getValue()));
                        break;
                    case INCLUDE:
                        boolean isString = ObjectUtil.isNull(item.getDefaultValueType()) || FormDataFilterParams.DefaultValueTypeEnum.STRING == item.getDefaultValueType();
                        if (isString) {
                            Pattern pattern = Pattern.compile("^.*" + item.getValue().toString() + ".*$", Pattern.CASE_INSENSITIVE);
                            criteriaList.add(Criteria.where(formItemId).regex(pattern));
                        } else {
                            criteriaList.add(Criteria.where(formItemId).in(item.getValue()));
                        }
                        break;
                    case NOT_INCLUDE:
                        boolean valueIsString = ObjectUtil.isNull(item.getDefaultValueType()) || FormDataFilterParams.DefaultValueTypeEnum.STRING == item.getDefaultValueType();
                        if (valueIsString) {
                            Pattern pattern = Pattern.compile("^((?!" + item.getValue().toString() + ").)*$", Pattern.CASE_INSENSITIVE);
                            criteriaList.add(Criteria.where(formItemId).regex(pattern));
                        } else {
                            criteriaList.add(Criteria.where(formItemId).nin(item.getValue()));
                        }
                        break;
                    case IS_NULL:
                        criteriaList.add(Criteria.where(formItemId).is(null).andOperator(Criteria.where(formItemId).exists(false)));
                        break;
                    case NOT_NULL:
                        criteriaList.add(Criteria.where(formItemId).ne(null).andOperator(Criteria.where(formItemId).exists(true)));
                        break;
                    case GT:
                        criteriaList.add(Criteria.where(formItemId).gt(item.getValue()));
                        break;
                    case GE:
                        criteriaList.add(Criteria.where(formItemId).gte(item.getValue()));
                        break;
                    case LT:
                        criteriaList.add(Criteria.where(formItemId).lt(item.getValue()));
                        break;
                    case LE:
                        criteriaList.add(Criteria.where(formItemId).lte(item.getValue()));
                        break;
                    case TIME_RANGE:
                        Criteria timeRangeCriteria = buildTimeRangeCriteria(item);
                        if (null != timeRangeCriteria) {
                            criteriaList.add(timeRangeCriteria);
                        }
                        break;
                    default:
                        break;
                }
            }
        }
        if (rel == FormDataFilterParams.RelEnum.AND) {
            criteria = criteria.andOperator(criteriaList.toArray(new Criteria[0]));
        } else {
            criteria = criteria.orOperator(criteriaList.toArray(new Criteria[0]));
        }
        return criteria;
    }


    public void appendOperator(Criteria criteria, Criteria subCriteria, FormDataFilterParams.RelEnum rel) {
        criteria = StrUtil.isBlank(criteria.getKey()) ? subCriteria : criteria;
        if (rel == FormDataFilterParams.RelEnum.AND) {
            criteria.andOperator(subCriteria);
        } else {
            criteria.orOperator(subCriteria);
        }
    }


    private Criteria buildTimeRangeCriteria(FormDataFilterParams.Condition condition) {
        if (ObjectUtil.isNull(condition.getValue())) {
            return null;
        }
        DateTime beginDate = null;
        DateTime endDate = null;
        TimeRangeEnum timeRangeEnum = TimeRangeEnum.valueOf(condition.getValue().toString());
        switch (timeRangeEnum) {
            case TODAY:
                beginDate = DateUtil.beginOfDay(new Date());
                endDate = DateUtil.endOfDay(new Date());
                break;
            case YESTERDAY:
                beginDate = DateUtil.beginOfDay(DateUtil.yesterday());
                endDate = DateUtil.endOfDay(DateUtil.yesterday());
                break;
            case WEEK:
                beginDate = DateUtil.beginOfWeek(new Date());
                endDate = DateUtil.endOfWeek(new Date());
                break;
            case LAST_WEEK:
                beginDate = DateUtil.beginOfWeek(DateUtil.lastWeek());
                endDate = DateUtil.endOfWeek(DateUtil.lastWeek());
                break;
            case MONTH:
                beginDate = DateUtil.beginOfMonth(new Date());
                endDate = DateUtil.endOfMonth(new Date());
                break;
            case LAST_MONTH:
                beginDate = DateUtil.beginOfMonth(DateUtil.lastMonth());
                endDate = DateUtil.endOfMonth(DateUtil.lastMonth());
                break;
            case QUARTER:
                beginDate = DateUtil.beginOfQuarter(new Date());
                endDate = DateUtil.endOfQuarter(new Date());
                break;
            case LAST_QUARTER:
                DateTime quarterLastMonth = DateUtil.beginOfQuarter(new Date()).offset(DateField.MONTH, -1);
                beginDate = DateUtil.beginOfQuarter(quarterLastMonth);
                endDate = DateUtil.endOfQuarter(quarterLastMonth);
                break;
            case YEAR:
                beginDate = DateUtil.beginOfYear(new Date());
                endDate = DateUtil.endOfYear(new Date());
                break;
            case LAST_YEAR:
                Date lastYear = DateUtil.offset(new Date(), DateField.YEAR, -1);
                beginDate = DateUtil.beginOfYear(lastYear);
                endDate = DateUtil.endOfYear(lastYear);
                break;
            default:
                beginDate = DateUtil.date();
                endDate = DateUtil.date();
                break;
        }
        return Criteria.where(condition.getFormItemId()).gte(beginDate.toJdkDate()).lte(endDate.toJdkDate());
    }

}
